Passed
Push — master ( 826326...13b350 )
by Rafael S.
02:46
created

wavefile-converter.js ➔ truncateIntSamples   B

Complexity

Conditions 6

Size

Total Lines 12
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 9
dl 0
loc 12
rs 8.6666
c 0
b 0
f 0
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileConverter class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
import { changeBitDepth } from 'bitdepth';
31
import * as imaadpcm from 'imaadpcm';
32
import * as alawmulaw from 'alawmulaw';
33
import { unpackArrayTo } from 'byte-data';
34
import { WaveFileCueEditor } from './wavefile-cue-editor';
35
import { truncateIntSamples } from './parsers/truncate-samples';
36
import { validateSampleRate } from './validators/validate-sample-rate';
37
import { resample } from './resampler';
38
39
/**
40
 * A class to convert wav files to other types of wav files.
41
 * @extends WaveFileCueEditor
42
 * @ignore
43
 */
44
export class WaveFileConverter extends WaveFileCueEditor {
45
46
  /**
47
   * Force a file as RIFF.
48
   */
49
  toRIFF() {
50
    /** @type {!Float64Array} */
51
    let output = new Float64Array(
52
      outputSize_(this.data.samples.length, this.dataType.bits / 8));
53
    unpackArrayTo(this.data.samples, this.dataType, output,
54
      0, this.data.samples.length, false, false);
55
    this.fromExisting_(
56
      this.fmt.numChannels,
57
      this.fmt.sampleRate,
58
      this.bitDepth,
59
      output);
60
  }
61
62
  /**
63
   * Force a file as RIFX.
64
   */
65
  toRIFX() {
66
    /** @type {!Float64Array} */
67
    let output = new Float64Array(
68
      outputSize_(this.data.samples.length, this.dataType.bits / 8));
69
    unpackArrayTo(this.data.samples, this.dataType, output,
70
      0, this.data.samples.length, false, false);
71
    this.fromExisting_(
72
      this.fmt.numChannels,
73
      this.fmt.sampleRate,
74
      this.bitDepth,
75
      output,
76
      {container: 'RIFX'});
77
  }
78
79
  /**
80
   * Encode a 16-bit wave file as 4-bit IMA ADPCM.
81
   * @throws {Error} If sample rate is not 8000.
82
   * @throws {Error} If number of channels is not 1.
83
   */
84
  toIMAADPCM() {
85
    if (this.fmt.sampleRate !== 8000) {
86
      throw new Error(
87
        'Only 8000 Hz files can be compressed as IMA-ADPCM.');
88
    } else if (this.fmt.numChannels !== 1) {
89
      throw new Error(
90
        'Only mono files can be compressed as IMA-ADPCM.');
91
    } else {
92
      this.assure16Bit_();
93
      /** @type {!Int16Array} */
94
      let output = new Int16Array(
95
        outputSize_(this.data.samples.length, 2));
96
      unpackArrayTo(this.data.samples, this.dataType, output,
97
        0, this.data.samples.length, false, false);
98
      this.fromExisting_(
99
        this.fmt.numChannels,
100
        this.fmt.sampleRate,
101
        '4',
102
        imaadpcm.encode(output),
103
        {container: this.correctContainer_()});
104
    }
105
  }
106
107
  /**
108
   * Decode a 4-bit IMA ADPCM wave file as a 16-bit wave file.
109
   * @param {string} bitDepthCode The new bit depth of the samples.
110
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
111
   *    Optional. Default is 16.
112
   */
113
  fromIMAADPCM(bitDepthCode='16') {
114
    this.fromExisting_(
115
      this.fmt.numChannels,
116
      this.fmt.sampleRate,
117
      '16',
118
      imaadpcm.decode(this.data.samples, this.fmt.blockAlign),
119
      {container: this.correctContainer_()});
120
    if (bitDepthCode != '16') {
121
      this.toBitDepth(bitDepthCode);
122
    }
123
  }
124
125
  /**
126
   * Encode a 16-bit wave file as 8-bit A-Law.
127
   */
128
  toALaw() {
129
    this.assure16Bit_();
130
    /** @type {!Int16Array} */
131
    let output = new Int16Array(
132
      outputSize_(this.data.samples.length, 2));
133
    unpackArrayTo(this.data.samples, this.dataType, output,
134
        0, this.data.samples.length, false, false);
135
    this.fromExisting_(
136
      this.fmt.numChannels,
137
      this.fmt.sampleRate,
138
      '8a',
139
      alawmulaw.alaw.encode(output),
140
      {container: this.correctContainer_()});
141
  }
142
143
  /**
144
   * Decode a 8-bit A-Law wave file into a 16-bit wave file.
145
   * @param {string} bitDepthCode The new bit depth of the samples.
146
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
147
   *    Optional. Default is 16.
148
   */
149
  fromALaw(bitDepthCode='16') {
150
    this.fromExisting_(
151
      this.fmt.numChannels,
152
      this.fmt.sampleRate,
153
      '16',
154
      alawmulaw.alaw.decode(this.data.samples),
155
      {container: this.correctContainer_()});
156
    if (bitDepthCode != '16') {
157
      this.toBitDepth(bitDepthCode);
158
    }
159
  }
160
161
  /**
162
   * Encode 16-bit wave file as 8-bit mu-Law.
163
   */
164
  toMuLaw() {
165
    this.assure16Bit_();
166
    /** @type {!Int16Array} */
167
    let output = new Int16Array(
168
      outputSize_(this.data.samples.length, 2));
169
    unpackArrayTo(this.data.samples, this.dataType, output,
170
        0, this.data.samples.length, false, false);
171
    this.fromExisting_(
172
      this.fmt.numChannels,
173
      this.fmt.sampleRate,
174
      '8m',
175
      alawmulaw.mulaw.encode(output),
176
      {container: this.correctContainer_()});
177
  }
178
179
  /**
180
   * Decode a 8-bit mu-Law wave file into a 16-bit wave file.
181
   * @param {string} bitDepthCode The new bit depth of the samples.
182
   *    One of '8' ... '32' (integers), '32f' or '64' (floats).
183
   *    Optional. Default is 16.
184
   */
185
  fromMuLaw(bitDepthCode='16') {
186
    this.fromExisting_(
187
      this.fmt.numChannels,
188
      this.fmt.sampleRate,
189
      '16',
190
      alawmulaw.mulaw.decode(this.data.samples),
191
      {container: this.correctContainer_()});
192
    if (bitDepthCode != '16') {
193
      this.toBitDepth(bitDepthCode);
194
    }
195
  }
196
197
  /**
198
   * Change the bit depth of the samples.
199
   * @param {string} newBitDepth The new bit depth of the samples.
200
   *    One of '8' ... '32' (integers), '32f' or '64' (floats)
201
   * @param {boolean} changeResolution A boolean indicating if the
202
   *    resolution of samples should be actually changed or not.
203
   * @throws {Error} If the bit depth is not valid.
204
   */
205
  toBitDepth(newBitDepth, changeResolution=true) {
206
    /** @type {string} */
207
    let toBitDepth = newBitDepth;
208
    /** @type {string} */
209
    let thisBitDepth = this.bitDepth;
210
    if (!changeResolution) {
211
      if (newBitDepth != '32f') {
212
        toBitDepth = this.dataType.bits.toString();
213
      }
214
      thisBitDepth = '' + this.dataType.bits;
215
    }
216
    // If the file is compressed, make it
217
    // PCM before changing the bit depth
218
    this.assureUncompressed_();
219
    /**
220
     * The original samples, interleaved.
221
     * @type {!Array|!TypedArray}
222
     */
223
    let samples = this.getSamples(true);
224
    /**
225
     * The container for the new samples.
226
     * @type {!Float64Array}
227
     */
228
    let newSamples = new Float64Array(samples.length);
229
    // Change the bit depth
230
    changeBitDepth(samples, thisBitDepth, newSamples, toBitDepth);
231
    // Re-create the file
232
    this.fromExisting_(
233
      this.fmt.numChannels,
234
      this.fmt.sampleRate,
235
      newBitDepth,
236
      newSamples,
237
      {container: this.correctContainer_()});
238
  }
239
240
  /**
241
   * Convert the sample rate of the file.
242
   * @param {number} sampleRate The target sample rate.
243
   * @param {?Object} details The extra configuration, if needed.
244
   */
245
  toSampleRate(sampleRate, details={}) {
246
    this.validateResample_(sampleRate);
247
    /** @type {!Array|!TypedArray} */
248
    let samples = this.getSamples();
249
    /** @type {!Array|!Float64Array} */
250
    let newSamples = [];
251
    // Mono files
252
    if (samples.constructor === Float64Array) {
253
      newSamples = resample(samples, this.fmt.sampleRate, sampleRate, details);
254
    // Multi-channel files
255
    } else {
256
      for (let i = 0; i < samples.length; i++) {
257
        newSamples.push(resample(
258
          samples[i], this.fmt.sampleRate, sampleRate, details));
259
      }
260
    }
261
    // Truncate samples
262
    if (this.bitDepth !== '64' && this.bitDepth !== '32f') {
263
      // Truncate samples in mono files
264
      if (newSamples[0].constructor === Number) {
265
        truncateIntSamples(newSamples, this.dataType.bits);
266
      // Truncate samples in multi-channel files
267
      } else {
268
        for (let i = 0; i < newSamples.length; i++) {
269
          truncateIntSamples(newSamples[i], this.dataType.bits);
270
        }
271
      }
272
    }
273
    // Recreate the file
274
    this.fromExisting_(
275
      this.fmt.numChannels, sampleRate, this.bitDepth, newSamples,
276
      {'container': this.correctContainer_()});
277
  }
278
279
  /**
280
   * Validate the conditions for resampling.
281
   * @param {number} sampleRate The target sample rate.
282
   * @throws {Error} If the file cant be resampled.
283
   * @private
284
   */
285
  validateResample_(sampleRate) {
286
    if (!validateSampleRate(
287
        this.fmt.numChannels, this.fmt.bitsPerSample, sampleRate)) {
288
      throw new Error('Invalid sample rate.');
289
    } else if (['4','8a','8m'].indexOf(this.bitDepth) > -1) {
290
      throw new Error(
291
        'wavefile can\'t change the sample rate of compressed files.');
292
    }
293
  }
294
295
  /**
296
   * Make the file 16-bit if it is not.
297
   * @private
298
   */
299
  assure16Bit_() {
300
    this.assureUncompressed_();
301
    if (this.bitDepth != '16') {
302
      this.toBitDepth('16');
303
    }
304
  }
305
306
  /**
307
   * Uncompress the samples in case of a compressed file.
308
   * @private
309
   */
310
  assureUncompressed_() {
311
    if (this.bitDepth == '8a') {
312
      this.fromALaw();
313
    } else if (this.bitDepth == '8m') {
314
      this.fromMuLaw();
315
    } else if (this.bitDepth == '4') {
316
      this.fromIMAADPCM();
317
    }
318
  }
319
320
  /**
321
   * Return 'RIFF' if the container is 'RF64', the current container name
322
   * otherwise. Used to enforce 'RIFF' when RF64 is not allowed.
323
   * @return {string}
324
   * @private
325
   */
326
  correctContainer_() {
327
    return this.container == 'RF64' ? 'RIFF' : this.container;
328
  }
329
330
  /**
331
   * Set up the WaveFileCreator object based on the arguments passed.
332
   * This method only reset the fmt , fact, ds64 and data chunks.
333
   * @param {number} numChannels The number of channels
334
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
335
   * @param {number} sampleRate The sample rate.
336
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
337
   * @param {string} bitDepthCode The audio bit depth code.
338
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
339
   *    or any value between '8' and '32' (like '12').
340
   * @param {!Array|!TypedArray} samples
341
   *    The samples. Must be in the correct range according to the bit depth.
342
   * @param {?Object} options Optional. Used to force the container
343
   *    as RIFX with {'container': 'RIFX'}
344
   * @throws {Error} If any argument does not meet the criteria.
345
   * @private
346
   */
347
  fromExisting_(numChannels, sampleRate, bitDepthCode, samples, options={}) {
348
    let tmpWav = new WaveFileCueEditor();
349
    Object.assign(this.fmt, tmpWav.fmt);
350
    Object.assign(this.fact, tmpWav.fact);
351
    Object.assign(this.ds64, tmpWav.ds64);
352
    Object.assign(this.data, tmpWav.data);
353
    this.newWavFile_(numChannels, sampleRate, bitDepthCode, samples, options);
354
  }
355
}
356
357
/**
358
 * Return the size in bytes of the output sample array when applying
359
 * compression to 16-bit samples.
360
 * @return {number}
361
 * @private
362
 */
363
function outputSize_(byteLen, byteOffset) {
364
  /** @type {number} */
365
  let outputSize = byteLen / byteOffset;
366
  if (outputSize % 2) {
367
    outputSize++;
368
  }
369
  return outputSize;
370
}
371